﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Drawing.Imaging;

namespace GBPVRLibrary.UtilityLibrary.Drawing
{
    public class ImageTransforms
    {
        /// <summary>
        /// Creates a reflection of an image.
        /// </summary>
        /// <param name="image">The image you want to reflect.</param>
        /// <param name="reflectPercent">The size of the reflected image as a percentage of the original.</param>
        /// <param name="compressOriginal">Specifies whether or not to show the entire original image in the reflected image.</param>
        /// <returns>A reflection of the original image.</returns>
        public static Image GetImageReflection(Image image, int reflectPercent, bool compressOriginal)
        {
            return GetImageReflection(image, reflectPercent, 200, compressOriginal, 1);
        }

        /// <summary>
        /// Creates a reflection of an image.
        /// </summary>
        /// <param name="image">The image you want to reflect.</param>
        /// <param name="reflectPercent">The size of the reflected image as a percentage of the original.</param>
        /// <param name="startingAlpha">The alpha value at which to start the gradient fade.</param>
        /// <param name="compressOriginal">Specifies whether or not to show the entire original image in the reflected image.</param>
        /// <param name="Quality">the Quality of the reflect image (int percent, 1 being full quality, 0.5 being half the quality of the original image etc)</param>
        /// <returns>A reflection of the original image.</returns>
        public static Image GetImageReflection(Image image, int reflectPercent, int startingAlpha, bool compressOriginal, float Quality)
        {
            reflectPercent = (reflectPercent > 100) ? 100 : reflectPercent;
            reflectPercent = (reflectPercent < 1) ? 1 : reflectPercent;
            using (Bitmap imageCopy = FlipImage(image, RotateFlipType.RotateNoneFlipY, Quality))
            {
                Image faded = null;
                int completeHeight = image.Height;
                if (reflectPercent < 100)
                {
                    completeHeight = (int)((double)reflectPercent / 100 * image.Height);
                    int rHeight = (int)(((double)reflectPercent / 100 * image.Height) * Quality);
                    using (Bitmap imageReflection = new Bitmap((int)(image.Width * Quality), (int)(rHeight * Quality), PixelFormat.Format32bppArgb))
                    {
                        using (Graphics g = Graphics.FromImage(imageReflection))
                        {
                            Rectangle rect = new Rectangle(new Point(0, 0), imageReflection.Size);
                            Rectangle oRect = new Rectangle(new Point(0, 0), imageCopy.Size);
                            g.InterpolationMode = InterpolationMode.HighQualityBicubic;
                            if (compressOriginal)
                                g.DrawImage(imageCopy, rect, 0, 0, imageCopy.Width, imageCopy.Height, GraphicsUnit.Pixel);
                            else g.DrawImage(imageCopy, oRect);
                        }
                        faded = FadeImage(imageReflection, FadeDirections.Down, startingAlpha);
                    }
                }
                else
                {
                    faded = FadeImage(imageCopy, FadeDirections.Down, startingAlpha);
                }
                Image complete = new Bitmap(image.Width, image.Height);
                using (Graphics g = Graphics.FromImage(complete))
                {
                    g.DrawImage(faded, new Rectangle(0, 0, complete.Width, completeHeight));
                }
                return complete;
            }
        }

        /// <summary>
        /// Creates an image with a 45% reflection with a gradient alpha fade.
        /// </summary>
        /// <param name="image">The image you want to reflect.</param>
        /// <returns>A copy of the original image with a faded reflection underneath.</returns>
        public static Image ReflectImage(Image image)
        {
            return ReflectImage(image, 45, true);
        }

        /// <summary>
        /// Creates an image with a 45% reflection with a gradient alpha fade.
        /// </summary>
        /// <param name="image">The image you want to reflect.</param>
        /// <param name="compressOriginal">Specifies whether or not to show the entire original image in the reflected image.</param>
        /// <returns>A copy of the original image with a faded reflection underneath.</returns>
        public static Image ReflectImage(Image image, bool compressOriginal)
        {
            return ReflectImage(image, 45, compressOriginal);
        }

        /// <summary>
        /// Creates an image with a fading reflection starting at alpha 255.
        /// </summary>
        /// <param name="image">The image you want to reflect.</param>
        /// <param name="reflectPercent">The size of the reflected image as a percentage of the original.</param>
        /// <param name="compressOriginal">Specifies whether or not to show the entire original image in the reflected image.</param>
        /// <returns>A copy of the original image with a gradient faded reflection underneath.</returns>
        public static Image ReflectImage(Image image, int reflectPercent, bool compressOriginal)
        {
            reflectPercent = (reflectPercent > 100) ? 100 : reflectPercent;
            reflectPercent = (reflectPercent < 1) ? 1 : reflectPercent;
            double rPercent = (double)reflectPercent / 100;
            int rHeight = (int)(rPercent * image.Height);
            Bitmap reflectedImage = new Bitmap(image.Width, image.Height + rHeight, PixelFormat.Format32bppArgb);
            using (Graphics g = Graphics.FromImage(reflectedImage))
            {
                Rectangle rect = new Rectangle(0, 0, image.Width, image.Height);
                g.InterpolationMode = InterpolationMode.HighQualityBicubic;
                g.DrawImage(image, rect);
                g.DrawImage(GetImageReflection(image, reflectPercent, compressOriginal), 0, image.Height);
            }
            return reflectedImage;
        }

        /// <summary>
        /// Creates an image with a reflection.
        /// </summary>
        /// <param name="image">The image you want to reflect.</param>
        /// <param name="reflectPercent">The size of the reflected image as a percentage of the original.</param>
        /// <param name="startingAlpha">The alpha value at which to start the gradient fade.</param>
        /// <param name="compressOriginal">Specifies whether or not to show the entire original image in the reflected image.</param>
        /// <returns>A copy of the original image with a gradient faded reflection underneath.</returns>
        public static Image ReflectImage(Image image, int reflectPercent, int startingAlpha, bool compressOriginal)
        {
            return ReflectImage(image, reflectPercent, startingAlpha, compressOriginal, 1f);
        }

        /// <summary>
        /// Creates an image with a reflection.
        /// </summary>
        /// <param name="image">The image you want to reflect.</param>
        /// <param name="reflectPercent">The size of the reflected image as a percentage of the original.</param>
        /// <param name="startingAlpha">The alpha value at which to start the gradient fade.</param>
        /// <param name="compressOriginal">Specifies whether or not to show the entire original image in the reflected image.</param>
        /// <param name="Quality">the Quality of the reflect image (int percent, 1 being full quality, 0.5 being half the quality of the original image etc)</param>
        /// <returns>A copy of the original image with a gradient faded reflection underneath.</returns>
        public static Image ReflectImage(Image image, int reflectPercent, int startingAlpha, bool compressOriginal, float Quality)
        {
            reflectPercent = (reflectPercent > 100) ? 100 : reflectPercent;
            reflectPercent = (reflectPercent < 1) ? 1 : reflectPercent;
            double rPercent = (double)reflectPercent / 100;
            int rHeight = (int)(rPercent * image.Height);
            Bitmap reflectedImage = new Bitmap(image.Width, image.Height + rHeight, PixelFormat.Format32bppArgb);
            using (Graphics g = Graphics.FromImage(reflectedImage))
            {
                Rectangle rect = new Rectangle(0, 0, image.Width, image.Height);
                g.InterpolationMode = InterpolationMode.HighQualityBicubic;
                g.DrawImage(image, rect);
                g.DrawImage(GetImageReflection(image, reflectPercent, startingAlpha, compressOriginal, Quality), 0, image.Height);
            }
            return reflectedImage;
        }

        /// <summary>
        /// Creates an image with an alpha fade in the specified direction. Fade starts at alpha 255.
        /// </summary>
        /// <param name="image">The image on which you want to perform the gradient fade.</param>
        /// <param name="fadeDirection">Specifies whether or not to show the entire original image in the reflected image.</param>
        /// <returns>A copy of the original image with a gradient alpha fade.</returns>
        public static Image FadeImage(Image image, FadeDirections fadeDirection)
        {
            return FadeImage(image, fadeDirection, 255);
        }

        /// <summary>
        /// Creates an image with an alpha fade in the specified direction.
        /// </summary>
        /// <param name="image">The image on which you want to perform the gradient fade.</param>
        /// <param name="fadeDirection">Specifies whether or not to show the entire original image in the reflected image.</param>
        /// <param name="startingAlpha">The alpha value at which to start the gradient fade.</param>
        /// <returns>A copy of the original image with a gradient alpha fade.</returns>
        public static Image FadeImage(Image image, FadeDirections fadeDirection, int startingAlpha)
        {
            startingAlpha = (startingAlpha > 255) ? 255 : startingAlpha;
            startingAlpha = (startingAlpha < 0) ? 0 : startingAlpha;

            #region Determine the maximum number of lines in the fade direction.
            int linesToFade = 0;
            switch (fadeDirection)
            {
                case FadeDirections.DiagonalDownLeft:
                case FadeDirections.DiagonalDownRight:
                case FadeDirections.DiagonalUpLeft:
                case FadeDirections.DiagonalUpRight:
                    linesToFade = (image.Width > image.Height) ? image.Width : image.Height;
                    break;
                case FadeDirections.Left:
                case FadeDirections.Right:
                    linesToFade = image.Width;
                    break;
                case FadeDirections.Up:
                case FadeDirections.Down:
                    linesToFade = image.Height;
                    break;
            }
            #endregion

            #region Flip and rotate image if necessary.
            switch (fadeDirection)
            {
                case FadeDirections.DiagonalDownLeft:
                case FadeDirections.DiagonalDownRight:
                case FadeDirections.DiagonalUpLeft:
                case FadeDirections.DiagonalUpRight:
                    throw new Exception("Method does not yet implement fading in this direction.");
                case FadeDirections.Left:
                    image.RotateFlip(RotateFlipType.RotateNoneFlipX);
                    break;
                case FadeDirections.Up:
                    image.RotateFlip(RotateFlipType.RotateNoneFlipY);
                    break;
                default:
                    // Nothing to do.
                    break;
            }
            #endregion

            #region Create faded copy
            Bitmap fadedImage = new Bitmap(image.Width, image.Height, PixelFormat.Format32bppArgb);
            using (Graphics g = Graphics.FromImage(fadedImage))
            {
                g.InterpolationMode = InterpolationMode.HighQualityBicubic;
                double startingAlphaF = startingAlpha / 255.0;
                double alphaStep =  (startingAlphaF) / linesToFade;
                int x = 0;
                while (x < linesToFade + 1) 
                {
                    double f = (x * alphaStep);
                    PointF[] p = { new PointF(), new PointF(), new PointF() };
                    RectangleF srcRect = new RectangleF(0, 0, fadedImage.Width, fadedImage.Height);

                    #region Get the current block on which to set the alpha
                    switch (fadeDirection)
                    {
                        case FadeDirections.DiagonalDownLeft:
                        case FadeDirections.DiagonalDownRight:
                        case FadeDirections.DiagonalUpLeft:
                        case FadeDirections.DiagonalUpRight:
                            throw new Exception("Method does not yet implement fading in this direction.");
                        case FadeDirections.Up:
                        case FadeDirections.Down:
                            p[0] = new PointF(0, (float)x);
                            p[1] = new PointF(fadedImage.Width, (float)x);
                            p[2] = new PointF(0, (float)x + (float)(fadedImage.Height * alphaStep));
                            srcRect.Y = (float)x;
                            srcRect.Height = (float)(fadedImage.Height * alphaStep);
                            break;
                        default: // FadeDirections.Right and FadeDirections.Left:
                            p[0] = new PointF((float)x, 0);
                            p[1] = new PointF((float)x + (float)(fadedImage.Width * alphaStep), 0);
                            p[2] = new PointF((float)x, fadedImage.Height);
                            srcRect.X = (float)x;
                            srcRect.Width = (float)(fadedImage.Width * alphaStep);
                            break;
                    }
                    #endregion

                    #region Set the alpha value for this block of pixels
                    float[][] matrixItems =
                    {
                        new float[] {1, 0, 0, 0, 0},
                        new float[] {0, 1, 0, 0, 0},
                        new float[] {0, 0, 1, 0, 0},
                        new float[] {0, 0, 0, (float)(startingAlphaF - (x * alphaStep)), 0}, // This is where the magic happens.
                        new float[] {0, 0, 0, 0, 1}
                    };
                    ColorMatrix colorMatrix = new ColorMatrix(matrixItems);
                    ImageAttributes imageAtt = new ImageAttributes();
                    imageAtt.SetColorMatrix(colorMatrix, ColorMatrixFlag.Default, ColorAdjustType.Bitmap);
                    #endregion

                    g.DrawImage(image, p, srcRect, GraphicsUnit.Pixel, imageAtt);
                    x++;
                }
            }
            #endregion

            #region Return original and faded copy to proper orientation if necessary.
            switch (fadeDirection)
            {
                case FadeDirections.DiagonalDownLeft:
                case FadeDirections.DiagonalDownRight:
                case FadeDirections.DiagonalUpLeft:
                case FadeDirections.DiagonalUpRight:
                    throw new Exception("Method does not yet implement fading in this direction.");
                case FadeDirections.Left:
                    image.RotateFlip(RotateFlipType.RotateNoneFlipX);
                    fadedImage.RotateFlip(RotateFlipType.RotateNoneFlipX);
                    break;
                case FadeDirections.Up:
                    image.RotateFlip(RotateFlipType.RotateNoneFlipY);
                    fadedImage.RotateFlip(RotateFlipType.RotateNoneFlipY);
                    break;
                default:
                    // Nothing to do.
                    break;
            }
            #endregion

            return fadedImage;
        }

        /// <summary>
        /// Takes an image and makes a rotated version of it
        /// </summary>
        /// <param name="ImageToRotate">the image to rotate</param>
        /// <param name="FlipType">the flip type of the image</param>
        /// <returns>the flipped imaged</returns>
        private static Bitmap FlipImage(Image ImageToRotate, RotateFlipType FlipType)
        {
            return FlipImage(ImageToRotate, FlipType, 1);
        }

        /// <summary>
        /// Takes an image and makes a rotated version of it
        /// </summary>
        /// <param name="ImageToRotate">the image to rotate</param>
        /// <param name="FlipType">the flip type of the image</param>
        /// <param name="Quality">the Quality of the reflect image (int percent, 1 being full quality, 0.5 being half the quality of the original image etc)</param>
        /// <returns>the flipped imaged</returns>
        private static Bitmap FlipImage(Image ImageToRotate, RotateFlipType FlipType, float Quality)
        {
            Bitmap flipped = new Bitmap((int)(ImageToRotate.Width * Quality), (int)(ImageToRotate.Height * Quality));
            using (Graphics g = Graphics.FromImage(flipped))
            {
                g.DrawImage(ImageToRotate, new Rectangle(0, 0, flipped.Width, flipped.Height));
            }
            flipped.RotateFlip(FlipType);
            return flipped;
        }

        public static Image GenerateComposite(List<Image> Images)
        {
            if (Images == null || Images.Count == 0)
                return null;
            else if (Images.Count == 1)
                return Images[0];
            
            double angle = -15;
            double step = (angle * 2) / Images.Count;
            List<Bitmap> rotated = (from i in Images select RotateImage(i, angle -= step, true)).ToList();


            using (Bitmap composite = new Bitmap((from i in rotated orderby i.Width descending select i.Width).First(), (from i in rotated orderby i.Height descending select i.Height).First()))
            {
                using (Graphics g = Graphics.FromImage(composite))
                {
                    g.CompositingQuality = CompositingQuality.HighQuality;
                    g.InterpolationMode = InterpolationMode.HighQualityBilinear;
                    g.SmoothingMode = SmoothingMode.AntiAlias;
                    for (int i = Images.Count - 1; i >= 0; i--)
                    {
                        g.DrawImage(rotated[i], 
                            new PointF(((float)rotated[i].Width - composite.Width) / 2, 
                                ((float)rotated[i].Height - composite.Height) / 2));
                    }
                }
                foreach (Bitmap b in rotated)
                    b.Dispose();
                rotated.Clear();
                rotated = null;
                using (System.IO.MemoryStream ms = new System.IO.MemoryStream())
                {
                    composite.Save(ms, ImageFormat.Png);
                    ms.Position = 0;
                    return Image.FromStream(ms);
                }

            }
            
        }

        public static Bitmap RotateImage(Image image, double angle, bool highDetail)
        {
            if (image == null)
                throw new ArgumentNullException("image");

            const double pi2 = Math.PI / 2.0D;

            // Why can't C# allow these to be const, or at least readonly
            // *sigh*  I'm starting to talk like Christian Graus OMG
            double oldWidth = (double)image.Width;
            double oldHeight = (double)image.Height;

            // Convert degrees to radians
            double theta = angle * Math.PI / 180.0D;
            double locked_theta = theta;

            // Ensure theta is now [0, 2pi)
            while (locked_theta < 0.0D)
                locked_theta += 2.0D * Math.PI;

            double newWidth, newHeight;
            int nWidth, nHeight; // The newWidth/newHeight expressed as ints

            #region Explaination of the calculations
            /*
			 * The trig involved in calculating the new width and height
			 * is fairly simple; the hard part was remembering that when 
			 * PI/2 <= theta <= PI and 3PI/2 <= theta < 2PI the width and 
			 * height are switched.
			 * 
			 * When you rotate a rectangle, r, the bounding box surrounding r
			 * contains for right-triangles of empty space.  Each of the 
			 * triangles hypotenuse's are a known length, either the width or
			 * the height of r.  Because we know the length of the hypotenuse
			 * and we have a known angle of rotation, we can use the trig
			 * function identities to find the length of the other two sides.
			 * 
			 * sine = opposite/hypotenuse
			 * cosine = adjacent/hypotenuse
			 * 
			 * solving for the unknown we get
			 * 
			 * opposite = sine * hypotenuse
			 * adjacent = cosine * hypotenuse
			 * 
			 * Another interesting point about these triangles is that there
			 * are only two different triangles. The proof for which is easy
			 * to see, but its been too long since I've written a proof that
			 * I can't explain it well enough to want to publish it.  
			 * 
			 * Just trust me when I say the triangles formed by the lengths 
			 * width are always the same (for a given theta) and the same 
			 * goes for the height of r.
			 * 
			 * Rather than associate the opposite/adjacent sides with the
			 * width and height of the original bitmap, I'll associate them
			 * based on their position.
			 * 
			 * adjacent/oppositeTop will refer to the triangles making up the 
			 * upper right and lower left corners
			 * 
			 * adjacent/oppositeBottom will refer to the triangles making up 
			 * the upper left and lower right corners
			 * 
			 * The names are based on the right side corners, because thats 
			 * where I did my work on paper (the right side).
			 * 
			 * Now if you draw this out, you will see that the width of the 
			 * bounding box is calculated by adding together adjacentTop and 
			 * oppositeBottom while the height is calculate by adding 
			 * together adjacentBottom and oppositeTop.
			 */
            #endregion

            double adjacentTop, oppositeTop;
            double adjacentBottom, oppositeBottom;

            // We need to calculate the sides of the triangles based
            // on how much rotation is being done to the bitmap.
            //   Refer to the first paragraph in the explaination above for 
            //   reasons why.
            if ((locked_theta >= 0.0D && locked_theta < pi2) ||
                (locked_theta >= Math.PI && locked_theta < (Math.PI + pi2)))
            {
                adjacentTop = Math.Abs(Math.Cos(locked_theta)) * oldWidth;
                oppositeTop = Math.Abs(Math.Sin(locked_theta)) * oldWidth;

                adjacentBottom = Math.Abs(Math.Cos(locked_theta)) * oldHeight;
                oppositeBottom = Math.Abs(Math.Sin(locked_theta)) * oldHeight;
            }
            else
            {
                adjacentTop = Math.Abs(Math.Sin(locked_theta)) * oldHeight;
                oppositeTop = Math.Abs(Math.Cos(locked_theta)) * oldHeight;

                adjacentBottom = Math.Abs(Math.Sin(locked_theta)) * oldWidth;
                oppositeBottom = Math.Abs(Math.Cos(locked_theta)) * oldWidth;
            }

            newWidth = adjacentTop + oppositeBottom;
            newHeight = adjacentBottom + oppositeTop;

            nWidth = (int)newWidth;
            nHeight = (int)newHeight;

            Bitmap rotatedBmp = new Bitmap(nWidth, nHeight);

            using (Graphics g = Graphics.FromImage(rotatedBmp))
            {
                g.PixelOffsetMode = PixelOffsetMode.HighQuality;
                if (highDetail)
                {
                    g.SmoothingMode = SmoothingMode.HighQuality;
                    g.InterpolationMode = InterpolationMode.HighQualityBicubic;
                }

                else
                {
                    g.SmoothingMode = SmoothingMode.None;
                    g.InterpolationMode = InterpolationMode.Low;
                }

                // This array will be used to pass in the three points that 
                // make up the rotated image
                PointF[] points;

                /*
                 * The values of opposite/adjacentTop/Bottom are referring to 
                 * fixed locations instead of in relation to the
                 * rotating image so I need to change which values are used
                 * based on the how much the image is rotating.
                 * 
                 * For each point, one of the coordinates will always be 0, 
                 * nWidth, or nHeight.  This because the Bitmap we are drawing on
                 * is the bounding box for the rotated bitmap.  If both of the 
                 * corrdinates for any of the given points wasn't in the set above
                 * then the bitmap we are drawing on WOULDN'T be the bounding box
                 * as required.
                 */
                if (locked_theta >= 0.0D && locked_theta < pi2)
                {
                    points = new PointF[] { 
											 new PointF( (float)oppositeBottom, 0.0F ), 
											 new PointF( (float)newWidth, (float)oppositeTop ),
											 new PointF( 0.0F, (float) adjacentBottom )
										 };

                }
                else if (locked_theta >= pi2 && locked_theta < Math.PI)
                {
                    points = new PointF[] { 
											 new PointF( (float)newWidth, (float) oppositeTop ),
											 new PointF( (float) adjacentTop, (float)newHeight ),
											 new PointF( (float) oppositeBottom, 0.0F )						 
										 };
                }
                else if (locked_theta >= Math.PI && locked_theta < (Math.PI + pi2))
                {
                    points = new PointF[] { 
											 new PointF( (float) adjacentTop, (float)newHeight ), 
											 new PointF( 0.0F, (float) adjacentBottom ),
											 new PointF( (float)newWidth, (float)oppositeTop )
										 };
                }
                else
                {
                    points = new PointF[] { 
											 new PointF( 0.0F, (float) adjacentBottom ), 
											 new PointF( (float) oppositeBottom, 0.0F ),
											 new PointF( (float) adjacentTop, (float)newHeight )		
										 };
                }
                g.DrawImage(image, points);
            }
            return rotatedBmp;
        }
    }
}
